// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.customtabs;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;

import android.app.Activity;
import android.app.Instrumentation;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.test.InstrumentationRegistry;

import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;

import org.chromium.base.IntentUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.app.metrics.LaunchCauseMetrics;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider.CustomTabsUiType;
import org.chromium.chrome.browser.customtabs.content.CustomTabIntentHandler;
import org.chromium.chrome.browser.customtabs.dependency_injection.BaseCustomTabActivityModule;
import org.chromium.chrome.browser.dependency_injection.ModuleOverridesRule;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.init.StartupTabPreloader;
import org.chromium.chrome.browser.test.ScreenShooter;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.chrome.browser.ui.appmenu.AppMenuCoordinator;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.browser.ui.appmenu.AppMenuItemProperties;
import org.chromium.chrome.browser.ui.appmenu.AppMenuPropertiesDelegate;
import org.chromium.chrome.browser.ui.appmenu.AppMenuTestSupport;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.concurrent.TimeoutException;

/**
 * Integration tests for the Custom Tab App Menu.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
public class CustomTabActivityAppMenuTest {
    private static final int MAX_MENU_CUSTOM_ITEMS = 5;
    private static final int NUM_CHROME_MENU_ITEMS = 5;
    private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
    private static final String TEST_MENU_TITLE = "testMenuTitle";

    public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule();

    private final TestRule mModuleOverridesRule = new ModuleOverridesRule().setOverride(
            BaseCustomTabActivityModule.Factory.class,
            (BrowserServicesIntentDataProvider intentDataProvider,
                    StartupTabPreloader startupTabPreloader,
                    CustomTabNightModeStateController nightModeController,
                    CustomTabIntentHandler.IntentIgnoringCriterion intentIgnoringCriterion,
                    TopUiThemeColorProvider topUiThemeColorProvider,
                    DefaultBrowserProviderImpl customTabDefaultBrowserProvider)
                    -> new BaseCustomTabActivityModule(intentDataProvider, startupTabPreloader,
                            nightModeController, intentIgnoringCriterion, topUiThemeColorProvider,
                            new FakeDefaultBrowserProviderImpl()));

    @Rule
    public RuleChain mRuleChain = RuleChain.emptyRuleChain()
                                          .around(mCustomTabActivityTestRule)
                                          .around(mModuleOverridesRule);

    @Rule
    public final ScreenShooter mScreenShooter = new ScreenShooter();

    private String mTestPage;

    @Before
    public void setUp() throws Exception {
        TestThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(true));
        mTestPage = mCustomTabActivityTestRule.getTestServer().getURL(TEST_PAGE);
        LibraryLoader.getInstance().ensureInitialized();
    }

    @After
    public void tearDown() {
        TestThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));

        // finish() is called on a non-UI thread by the testing harness. Must hide the menu
        // first, otherwise the UI is manipulated on a non-UI thread.
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            if (mCustomTabActivityTestRule.getActivity() == null) return;
            AppMenuCoordinator coordinator = mCustomTabActivityTestRule.getAppMenuCoordinator();
            // CCT doesn't always have a menu (ex. in the media viewer).
            if (coordinator == null) return;
            AppMenuHandler handler = coordinator.getAppMenuHandler();
            if (handler != null) handler.hideAppMenu();
        });
    }

    private Intent createMinimalCustomTabIntent() {
        return CustomTabsTestUtils.createMinimalCustomTabIntent(
                InstrumentationRegistry.getTargetContext(), mTestPage);
    }

    private CustomTabIntentDataProvider getCustomTabIntentDataProvider() {
        return (CustomTabIntentDataProvider) mCustomTabActivityTestRule.getActivity()
                .getIntentDataProvider();
    }

    private void openAppMenuAndAssertMenuShown() {
        CustomTabsTestUtils.openAppMenuAndAssertMenuShown(mCustomTabActivityTestRule.getActivity());
    }

    /**
     * Test the entries in the app menu.
     */
    @Test
    @SmallTest
    public void testAppMenu() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        int numMenuEntries = 1;
        CustomTabsTestUtils.addMenuEntriesToIntent(intent, numMenuEntries, TEST_MENU_TITLE);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        openAppMenuAndAssertMenuShown();
        ModelList menuItemsModelList = AppMenuTestSupport.getMenuModelList(
                mCustomTabActivityTestRule.getAppMenuCoordinator());
        final int expectedMenuSize = numMenuEntries + NUM_CHROME_MENU_ITEMS;

        Assert.assertNotNull("App menu is not initialized: ", menuItemsModelList);
        assertEquals(expectedMenuSize, menuItemsModelList.size());
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.forward_menu_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.bookmark_this_page_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.offline_page_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.info_menu_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.reload_menu_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.open_in_browser_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.find_in_page_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.add_to_homescreen_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(),
                R.id.request_desktop_site_row_menu_id));
        Assert.assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.share_row_menu_id));

        mScreenShooter.shoot("Testtttt");
    }

    /**
     * Test the App Menu does not show for media viewer.
     */
    @Test
    @SmallTest
    public void testAppMenuForMediaViewer() {
        Intent intent = createMinimalCustomTabIntent();
        intent.putExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE, CustomTabsUiType.MEDIA_VIEWER);
        IntentUtils.addTrustedIntentExtras(intent);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> {
            mCustomTabActivityTestRule.getActivity().onMenuOrKeyboardAction(R.id.show_menu, false);
            Assert.assertNull(mCustomTabActivityTestRule.getAppMenuCoordinator());
        });
    }

    /**
     * Test the entries in app menu for Reader Mode.
     */
    @Test
    @SmallTest
    public void testAppMenuForReaderMode() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        intent.putExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE, CustomTabsUiType.READER_MODE);
        IntentUtils.addTrustedIntentExtras(intent);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        openAppMenuAndAssertMenuShown();
        ModelList menuItemsModelList = AppMenuTestSupport.getMenuModelList(
                mCustomTabActivityTestRule.getAppMenuCoordinator());
        final int expectedMenuSize = 2;

        Assert.assertNotNull("App menu is not initialized: ", menuItemsModelList);
        assertEquals(expectedMenuSize, menuItemsModelList.size());
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.find_in_page_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.reader_mode_prefs_id));
    }

    /**
     * Test the entries in app menu for media viewer.
     */
    @Test
    @SmallTest
    public void testAppMenuForOfflinePage() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        intent.putExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE,
                CustomTabIntentDataProvider.CustomTabsUiType.OFFLINE_PAGE);
        IntentUtils.addTrustedIntentExtras(intent);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        openAppMenuAndAssertMenuShown();
        ModelList menuItemsModelList = AppMenuTestSupport.getMenuModelList(
                mCustomTabActivityTestRule.getAppMenuCoordinator());
        final int expectedMenuSize = 3;

        Assert.assertNotNull("App menu is not initialized: ", menuItemsModelList);
        assertEquals(expectedMenuSize, menuItemsModelList.size());
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.find_in_page_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(),
                R.id.request_desktop_site_row_menu_id));

        ModelList iconRowModelList =
                AppMenuTestSupport
                        .getMenuItemPropertyModel(
                                mCustomTabActivityTestRule.getAppMenuCoordinator(),
                                R.id.icon_row_menu_id)
                        .get(AppMenuItemProperties.SUBMENU);
        final int expectedIconMenuSize = 4;
        assertEquals(expectedIconMenuSize, iconRowModelList.size());
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.forward_menu_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.bookmark_this_page_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.info_menu_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.reload_menu_id));
    }

    @Test
    @SmallTest
    public void testAppMenuBeforeFirstRun() throws Exception {
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(createMinimalCustomTabIntent());
        // Mark the first run as not completed. This has to be done after we start the intent,
        // otherwise we are going to hit the FRE.
        TestThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));

        openAppMenuAndAssertMenuShown();
        ModelList menuItemsModelList = AppMenuTestSupport.getMenuModelList(
                mCustomTabActivityTestRule.getAppMenuCoordinator());
        final int expectedMenuSize = 3;

        Assert.assertNotNull("App menu is not initialized: ", menuItemsModelList);
        assertEquals(expectedMenuSize, menuItemsModelList.size());
        // Checks the first row (icons).
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.forward_menu_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.info_menu_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.reload_menu_id));
        Assert.assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.offline_page_id));
        Assert.assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.bookmark_this_page_id));

        // Following rows.
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.find_in_page_id));
        Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(),
                R.id.request_desktop_site_row_menu_id));
        Assert.assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.open_in_browser_id));
        Assert.assertNull(AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.add_to_homescreen_id));
    }

    /**
     * Tests if the default share item can be shown in the app menu.
     */
    @Test
    @SmallTest
    public void testShareMenuItem() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        intent.putExtra(CustomTabsIntent.EXTRA_DEFAULT_SHARE_MENU_ITEM, true);
        intent.putExtra(CustomTabsIntent.EXTRA_SHARE_STATE, CustomTabsIntent.SHARE_STATE_OFF);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        openAppMenuAndAssertMenuShown();
        PropertyModel sharePropertyModel = AppMenuTestSupport.getMenuItemPropertyModel(
                mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.share_menu_id);
        Assert.assertNotNull(sharePropertyModel);
        Assert.assertTrue(sharePropertyModel.get(AppMenuItemProperties.ENABLED));
    }

    /**
     * Test that only up to 5 entries are added to the custom menu.
     */
    @Test
    @SmallTest
    public void testMaxMenuItems() throws Exception {
        Intent intent = createMinimalCustomTabIntent();
        int numMenuEntries = 7;
        Assert.assertTrue(MAX_MENU_CUSTOM_ITEMS < numMenuEntries);
        CustomTabsTestUtils.addMenuEntriesToIntent(intent, numMenuEntries, TEST_MENU_TITLE);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);

        openAppMenuAndAssertMenuShown();
        ModelList menuItemsModelList = AppMenuTestSupport.getMenuModelList(
                mCustomTabActivityTestRule.getAppMenuCoordinator());
        final int expectedMenuSize = MAX_MENU_CUSTOM_ITEMS + NUM_CHROME_MENU_ITEMS;
        Assert.assertNotNull("App menu is not initialized: ", menuItemsModelList);
        assertEquals(expectedMenuSize, menuItemsModelList.size());
    }

    /**
     * Test whether the custom menu is correctly shown and clicking it sends the right
     * {@link PendingIntent}.
     */
    @Test
    @SmallTest
    public void testCustomMenuEntry() throws TimeoutException {
        Intent customTabIntent = createMinimalCustomTabIntent();
        Intent baseCallbackIntent = new Intent();
        baseCallbackIntent.putExtra("FOO", 42);
        final PendingIntent pi = CustomTabsTestUtils.addMenuEntriesToIntent(
                customTabIntent, 1, baseCallbackIntent, TEST_MENU_TITLE);
        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(customTabIntent);

        final CustomTabsTestUtils.OnFinishedForTest onFinished =
                new CustomTabsTestUtils.OnFinishedForTest(pi);
        getCustomTabIntentDataProvider().setPendingIntentOnFinishedForTesting(onFinished);

        openAppMenuAndAssertMenuShown();
        PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> {
            int itemId = ((CustomTabAppMenuPropertiesDelegate)
                                  AppMenuTestSupport.getAppMenuPropertiesDelegate(
                                          mCustomTabActivityTestRule.getAppMenuCoordinator()))
                                 .getItemIdForTitle(TEST_MENU_TITLE);
            Assert.assertNotEquals(AppMenuPropertiesDelegate.INVALID_ITEM_ID, itemId);
            AppMenuTestSupport.onOptionsItemSelected(
                    mCustomTabActivityTestRule.getAppMenuCoordinator(), itemId);
        });

        onFinished.waitForCallback("Pending Intent was not sent.");
        Intent callbackIntent = onFinished.getCallbackIntent();
        Assert.assertThat(callbackIntent.getDataString(), equalTo(mTestPage));

        // Verify that the callback intent has the page title as the subject, but other extras are
        // kept intact.
        Assert.assertThat(
                callbackIntent.getStringExtra(Intent.EXTRA_SUBJECT), equalTo("The Google"));
        Assert.assertThat(callbackIntent.getIntExtra("FOO", 0), equalTo(42));
    }

    /**
     * Test whether clicking "Open in Chrome" takes us to a chrome normal tab, loading the same url.
     */
    @Test
    @SmallTest
    public void testOpenInBrowser() throws Exception {
        // Augment the CustomTabsSession to catch the callback.
        CallbackHelper callbackTriggered = new CallbackHelper();
        CustomTabsSession session =
                CustomTabsTestUtils
                        .bindWithCallback(new CustomTabsCallback() {
                            @Override
                            public void extraCallback(String callbackName, Bundle args) {
                                if (callbackName.equals(
                                            CustomTabsConnection.OPEN_IN_BROWSER_CALLBACK)) {
                                    callbackTriggered.notifyCalled();
                                }
                            }
                        })
                        .session;

        Intent intent = new CustomTabsIntent.Builder(session).build().intent;
        intent.setData(Uri.parse(mTestPage));
        intent.setComponent(new ComponentName(
                InstrumentationRegistry.getTargetContext(), ChromeLauncherActivity.class));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        IntentUtils.addTrustedIntentExtras(intent);

        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
        assertEquals(1,
                RecordHistogram.getHistogramValueCountForTesting(
                        LaunchCauseMetrics.LAUNCH_CAUSE_HISTOGRAM,
                        LaunchCauseMetrics.LaunchCause.CUSTOM_TAB));

        IntentFilter filter = new IntentFilter(Intent.ACTION_VIEW);
        filter.addDataScheme(
                Uri.parse(mCustomTabActivityTestRule.getTestServer().getURL("/")).getScheme());
        final Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation().addMonitor(filter, null, false);
        openAppMenuAndAssertMenuShown();
        PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> {
            Assert.assertNotNull(AppMenuTestSupport.getMenuItemPropertyModel(
                    mCustomTabActivityTestRule.getAppMenuCoordinator(), R.id.open_in_browser_id));
            mCustomTabActivityTestRule.getActivity().onMenuOrKeyboardAction(
                    R.id.open_in_browser_id, false);
        });
        final Activity activity =
                monitor.waitForActivityWithTimeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL);

        callbackTriggered.waitForCallback(0);

        CriteriaHelper.pollInstrumentationThread(() -> {
            Criteria.checkThat(RecordHistogram.getHistogramValueCountForTesting(
                                       LaunchCauseMetrics.LAUNCH_CAUSE_HISTOGRAM,
                                       LaunchCauseMetrics.LaunchCause.OPEN_IN_BROWSER_FROM_MENU),
                    Matchers.is(1));
        }, 5000L, CriteriaHelper.DEFAULT_POLLING_INTERVAL);
        activity.finish();
    }
}
